#!/usr/bin/env python
#
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import print_function

import collections
import functools
import multiprocessing
import optparse
import os
import platform
import re
import shutil
import subprocess
import sys


SCRIPTS_DIR = os.path.abspath(os.path.dirname(__file__))
FFMPEG_DIR = os.path.abspath(os.path.join(SCRIPTS_DIR, '..', '..'))


USAGE = """Usage: %prog TARGET_OS TARGET_ARCH [options]

Valid combinations are linux       [ia32|x64|mipsel|arm|arm-neon]
                       linux-noasm [ia32|x64]
                       mac         [ia32|x64]

 linux ia32/x64 - script can be run on a normal Ubuntu box.
 linux mipsel - script can be run on a normal Ubuntu box with MIPS
 cross-toolchain in $PATH.
 linux arm/arm-neon should be run inside of CrOS chroot.
 mac has to be run on Mac

 mac - ensure the Chromium (not Apple) version of clang is in the path,
 usually found under src/third_party/llvm-build/Release+Asserts/bin

Resulting binaries will be placed in:
  build.TARGET_ARCH.TARGET_OS/Chromium/out/
  build.TARGET_ARCH.TARGET_OS/Chrome/out/
  build.TARGET_ARCH.TARGET_OS/ChromiumOS/out/
  build.TARGET_ARCH.TARGET_OS/ChromeOS/out/"""


def PrintAndCheckCall(argv, *args, **kwargs):
  print('Running %r' % argv)
  subprocess.check_call(argv, *args, **kwargs)


def DetermineHostOsAndArch():
  if platform.system() == 'Linux':
    host_os = 'linux'
  elif platform.system() == 'Darwin':
    host_os = 'mac'
  else:
    return None

  if re.match(r'i.86', platform.machine()):
    host_arch = 'ia32'
  elif platform.machine() == 'x86_64':
    host_arch = 'x64'
  elif platform.machine().startswith('arm'):
    host_arch = 'arm'
  else:
    return None

  return (host_os, host_arch)


def GetDsoName(target_os, dso_name, dso_version):
  if target_os == 'linux':
    return 'lib%s.so.%s' % (dso_name, dso_version)
  elif target_os == 'mac':
    return 'lib%s.%s.dylib' % (dso_name, dso_version)
  else:
    raise ValueError('Unexpected target_os %s' % target_os)


def RewriteFile(path, search, replace):
  with open(path) as f:
    contents = f.read()
  with open(path, 'w') as f:
    f.write(re.sub(search, replace, contents))


def BuildFFmpeg(target_os, target_arch, host_os, host_arch, parallel_jobs,
                config_only, config, configure_flags):
  print('%s configure/build:' % config)

  config_dir = 'build.%s.%s/%s' % (target_arch, target_os, config)
  shutil.rmtree(config_dir, ignore_errors=True)
  os.makedirs(os.path.join(config_dir, 'out'))

  PrintAndCheckCall(
      [os.path.join(FFMPEG_DIR, 'configure')] + configure_flags, cwd=config_dir)

  if (target_os, target_arch) == ('mac', 'ia32'):
    # Required to get Mac ia32 builds compiling with -fno-omit-frame-pointer,
    # which is required for accurate stack traces.  See http://crbug.com/115170.
    #
    # Without this, building without -fomit-frame-pointer on ia32 will result in
    # the the inclusion of a number of inline assembly blocks that use too many
    # registers for its input/output operands.
    for name in ('config.h', 'config.asm'):
      RewriteFile(os.path.join(config_dir, name),
                  'HAVE_EBP_AVAILABLE 1',
                  'HAVE_EBP_AVAILABLE 0')

  if host_os == target_os and not config_only:
    libraries = [
        os.path.join('libavcodec', GetDsoName(target_os, 'avcodec', 55)),
        os.path.join('libavformat', GetDsoName(target_os, 'avformat', 55)),
        os.path.join('libavutil', GetDsoName(target_os, 'avutil', 52)),
    ]
    PrintAndCheckCall(
        ['make', '-j%d' % parallel_jobs] + libraries, cwd=config_dir)
    for lib in libraries:
      shutil.copy(os.path.join(config_dir, lib),
                  os.path.join(config_dir, 'out'))
  elif config_only:
    print('Skipping build step as requested.')
  else:
    print('Skipping compile as host configuration differs from target.\n'
          'Please compare the generated config.h with the previous version.\n'
          'You may also patch the script to properly cross-compile.\n'
          'Host OS : %s\n'
          'Target OS : %s\n'
          'Host arch : %s\n'
          'Target arch : %s\n' % (host_os, target_os, host_arch, target_arch))

  if target_arch in ('arm', 'arm-neon'):
    RewriteFile(
        os.path.join(config_dir, 'config.h'),
        r'(#define HAVE_VFP_ARGS [01])',
        r'/* \1 -- Disabled to allow softfp/hardfp selection at gyp time */')


def main(argv):
  parser = optparse.OptionParser(usage=USAGE)
  parser.add_option('--config-only', action='store_true',
                    help='Skip the build step. Useful when a given platform '
                    'is not necessary for generate_gyp.py')
  options, args = parser.parse_args(argv)

  if len(args) != 2:
    parser.print_help()
    return 1

  target_os = args[0]
  target_arch = args[1]

  if target_os not in ('linux', 'linux-noasm', 'win', 'win-vs2013', 'mac'):
    parser.print_help()
    return 1

  host_tuple = DetermineHostOsAndArch()
  if not host_tuple:
    print('Unrecognized host OS and architecture.', file=sys.stderr)
    return 1

  host_os, host_arch = host_tuple
  parallel_jobs = multiprocessing.cpu_count()

  print('System information:\n'
        'Host OS       : %s\n'
        'Target OS     : %s\n'
        'Host arch     : %s\n'
        'Target arch   : %s\n'
        'Parallel jobs : %d\n' % (
            host_os, target_os, host_arch, target_arch, parallel_jobs))

  configure_flags = collections.defaultdict(list)

  # Common configuration.  Note: --disable-everything does not in fact disable
  # everything, just non-library components such as decoders and demuxers.
  configure_flags['Common'].extend([
      '--disable-everything',
      '--disable-avdevice',
      '--disable-avfilter',
      '--disable-bzlib',
      '--disable-doc',
      '--disable-ffprobe',
      '--disable-lzo',
      '--disable-network',
      '--disable-postproc',
      '--disable-swresample',
      '--disable-swscale',
      '--disable-zlib',
      '--enable-fft',
      '--enable-rdft',
      '--enable-shared',
      '--disable-iconv',

      # Disable hardware decoding options which will sometimes turn on
      # via autodetect.
      '--disable-dxva2',
      '--disable-vaapi',
      '--disable-vda',
      '--disable-vdpau',

      # Common codecs.
      '--enable-decoder=theora,vorbis,vp8',
      '--enable-decoder=pcm_u8,pcm_s16le,pcm_s24le,pcm_f32le',
      '--enable-decoder=pcm_s16be,pcm_s24be,pcm_mulaw,pcm_alaw',
      '--enable-demuxer=ogg,matroska,wav',
      '--enable-parser=vp3,vorbis,vp8',
  ])

  # --optflags doesn't append multiple entries, so set all at once.
  if (target_os, target_arch) == ('mac', 'ia32'):
    configure_flags['Common'].append('--optflags="-fno-omit-frame-pointer -O2"')
  else:
    configure_flags['Common'].append('--optflags="-O2"')

  # Linux only.
  if target_os in ('linux', 'linux-noasm'):
    if target_arch == 'x64':
      pass
    elif target_arch == 'ia32':
      configure_flags['Common'].extend([
          '--arch=i686',
          '--enable-yasm',
          '--extra-cflags="-m32"',
          '--extra-ldflags="-m32"',
      ])
    elif target_arch == 'arm':
      if host_arch != 'arm':
        configure_flags['Common'].extend([
            # This if-statement essentially is for chroot tegra2.
            '--enable-cross-compile',

            # Location is for CrOS chroot. If you want to use this, enter chroot
            # and copy ffmpeg to a location that is reachable.
            '--cross-prefix=/usr/bin/armv7a-cros-linux-gnueabi-',
            '--target-os=linux',
            '--arch=arm',
        ])

      # TODO(ihf): ARM compile flags are tricky. The final options
      # overriding everything live in chroot /build/*/etc/make.conf
      # (some of them coming from src/overlays/overlay-<BOARD>/make.conf).
      # We try to follow these here closely. In particular we need to
      # set ffmpeg internal #defines to conform to make.conf.
      # TODO(ihf): For now it is not clear if thumb or arm settings would be
      # faster. I ran experiments in other contexts and performance seemed
      # to be close and compiler version dependent. In practice thumb builds are
      # much smaller than optimized arm builds, hence we go with the global
      # CrOS settings.
      configure_flags['Common'].extend([
          '--enable-armv6',
          '--enable-armv6t2',
          '--enable-vfp',
          '--enable-thumb',
          '--disable-neon',
          '--extra-cflags=-march=armv7-a',
          '--extra-cflags=-mtune=cortex-a8',
          '--extra-cflags=-mfpu=vfpv3-d16',
          # NOTE: softfp/hardfp selected at gyp time.
          '--extra-cflags=-mfloat-abi=hard',
      ])
    elif target_arch == 'arm-neon':
      if host_arch != 'arm':
        # This if-statement is for chroot arm-generic.
        configure_flags['Common'].extend([
            '--enable-cross-compile',
            '--cross-prefix=/usr/bin/armv7a-cros-linux-gnueabi-',
            '--target-os=linux',
            '--arch=arm',
        ])
      configure_flags['Common'].extend([
          '--enable-armv6',
          '--enable-armv6t2',
          '--enable-vfp',
          '--enable-thumb',
          '--enable-neon',
          '--extra-cflags=-march=armv7-a',
          '--extra-cflags=-mtune=cortex-a8',
          '--extra-cflags=-mfpu=neon',
          # NOTE: softfp/hardfp selected at gyp time.
          '--extra-cflags=-mfloat-abi=hard',
      ])
    elif target_arch == 'mipsel':
      configure_flags['Common'].extend([
          '--enable-cross-compile',
          '--cross-prefix=mips-linux-gnu-',
          '--target-os=linux',
          '--arch=mips',
          '--extra-cflags=-mips32',
          '--extra-cflags=-EL',
          '--extra-ldflags=-mips32',
          '--extra-ldflags=-EL',
          '--disable-mipsfpu',
          '--disable-mipsdspr1',
          '--disable-mipsdspr2',
      ])
    else:
      print('Error: Unknown target arch %r for target OS %r!' % (
          target_arch, target_os), file=sys.stderr)
      return 1

  if target_os == 'linux-noasm':
    configure_flags['Common'].extend([
        '--disable-asm',
        '--disable-inline-asm',
    ])

  if 'win' not in target_os:
    configure_flags['Common'].append('--enable-pic')

  # Should be run on Mac.
  if target_os == 'mac':
    if host_os != 'mac':
      print('Script should be run on a Mac host. If this is not possible\n'
            'try a merge of config files with new linux ia32 config.h\n'
            'by hand.\n', file=sys.stderr)
      return 1

    configure_flags['Common'].extend([
        '--enable-yasm',
        '--cc=clang',
        '--cxx=clang++',
    ])
    if target_arch == 'ia32':
      configure_flags['Common'].extend([
          '--arch=i686',
          '--extra-cflags=-m32',
          '--extra-ldflags=-m32',
      ])
    elif target_arch == 'x64':
      configure_flags['Common'].extend([
          '--arch=x86_64',
          '--extra-cflags=-m64',
          '--extra-ldflags=-m64',
      ])
    else:
      print('Error: Unknown target arch %r for target OS %r!' % (
          target_arch, target_os), file=sys.stderr)

  # Chromium & ChromiumOS specific configuration.
  # Though CONFIG_ERROR_RESILIENCE should already be disabled for Chromium[OS],
  # forcing disable here to help ensure it remains this way.
  configure_flags['Chromium'].append('--disable-error-resilience')

  # Google Chrome & ChromeOS specific configuration.
  configure_flags['Chrome'].extend([
      '--enable-decoder=aac,h264,mp3',
      '--enable-demuxer=mp3,mov',
      '--enable-parser=aac,h264,mpegaudio',
  ])

  # ChromiumOS specific configuration.
  # Warning: do *NOT* add avi, aac, h264, mp3, mp4, amr*
  # Flac support.
  configure_flags['ChromiumOS'].extend([
      '--enable-demuxer=flac',
      '--enable-decoder=flac',
      '--enable-parser=flac',
  ])

  # Google ChromeOS specific configuration.
  # We want to make sure to play everything Android generates and plays.
  # http://developer.android.com/guide/appendix/media-formats.html
  configure_flags['ChromeOS'].extend([
      # Enable playing avi files.
      '--enable-decoder=mpeg4',
      '--enable-parser=h263,mpeg4video',
      '--enable-demuxer=avi',
      # Enable playing Android 3gp files.
      '--enable-demuxer=amr',
      '--enable-decoder=amrnb,amrwb',
      # Flac support.
      '--enable-demuxer=flac',
      '--enable-decoder=flac',
      '--enable-parser=flac',
      # Wav files for playing phone messages.
      '--enable-decoder=gsm_ms',
      '--enable-demuxer=gsm',
      '--enable-parser=gsm',
  ])

  do_build_ffmpeg = functools.partial(
      BuildFFmpeg, target_os, target_arch, host_os, host_arch, parallel_jobs,
      options.config_only)
  do_build_ffmpeg(
      'Chromium', configure_flags['Common'] + configure_flags['Chromium'])
  do_build_ffmpeg(
      'Chrome', configure_flags['Common'] + configure_flags['Chrome'])

  if target_os == 'linux':
    do_build_ffmpeg('ChromiumOS',
                    configure_flags['Common'] +
                    configure_flags['Chromium'] +
                    configure_flags['ChromiumOS'])
    do_build_ffmpeg('ChromeOS',
                    configure_flags['Common'] +
                    configure_flags['Chrome'] +
                    configure_flags['ChromeOS'])

  print('Done. If desired you may copy config.h/config.asm into the '
        'source/config tree using copy_config.sh.')
  return 0


if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))
